#pragma once

#include <d3d12.h>
#include "Macros.h"
#include "Window.h"
#include "Libs.h"
#include "Shared.h"
#include "Scene/Scene.h"

#include "Scene/TextureLoader.h"

namespace SL
{

// Simple free list based allocator
struct ExampleDescriptorHeapAllocator
{
    ID3D12DescriptorHeap* Heap = nullptr;
    D3D12_DESCRIPTOR_HEAP_TYPE HeapType = D3D12_DESCRIPTOR_HEAP_TYPE_NUM_TYPES;
    D3D12_CPU_DESCRIPTOR_HANDLE HeapStartCpu;
    D3D12_GPU_DESCRIPTOR_HANDLE HeapStartGpu;
    UINT HeapHandleIncrement;
    std::vector<int> FreeIndices;

    void Create(ID3D12Device* device, ID3D12DescriptorHeap* heap)
    {
        assert(Heap == nullptr && FreeIndices.empty());
        Heap = heap;
        D3D12_DESCRIPTOR_HEAP_DESC desc = heap->GetDesc();
        HeapType = desc.Type;
        HeapStartCpu = Heap->GetCPUDescriptorHandleForHeapStart();
        HeapStartGpu = Heap->GetGPUDescriptorHandleForHeapStart();
        HeapHandleIncrement = device->GetDescriptorHandleIncrementSize(HeapType);
        FreeIndices.reserve((int)desc.NumDescriptors);
        for (int n = desc.NumDescriptors; n > 0; n--)
            FreeIndices.push_back(n);
    }
    void Destroy()
    {
        Heap = nullptr;
        FreeIndices.clear();
    }
    void Alloc(D3D12_CPU_DESCRIPTOR_HANDLE* out_cpu_desc_handle, D3D12_GPU_DESCRIPTOR_HANDLE* out_gpu_desc_handle)
    {
        IM_ASSERT(FreeIndices.size() > 0);
        int idx = FreeIndices.back();
        FreeIndices.pop_back();
        out_cpu_desc_handle->ptr = HeapStartCpu.ptr + (idx * HeapHandleIncrement);
        out_gpu_desc_handle->ptr = HeapStartGpu.ptr + (idx * HeapHandleIncrement);
    }
    void Free(D3D12_CPU_DESCRIPTOR_HANDLE out_cpu_desc_handle, D3D12_GPU_DESCRIPTOR_HANDLE out_gpu_desc_handle)
    {
        int cpu_idx = (int)((out_cpu_desc_handle.ptr - HeapStartCpu.ptr) / HeapHandleIncrement);
        int gpu_idx = (int)((out_gpu_desc_handle.ptr - HeapStartGpu.ptr) / HeapHandleIncrement);
        IM_ASSERT(cpu_idx == gpu_idx);
        FreeIndices.push_back(cpu_idx);
    }
};

struct AppConfig
{
    Window::Desc windowDesc;
    uint32_t backBufferNum = 2;  /// Number of back buffers. This is the number of buffers in the swap chain.
    uint32_t framesInFlight = 2; ///< Number of back buffers. This is the number of frames in flight.
    uint32_t refreshRate = 60;   ///< Refresh rate in full screen mode.
    std::string hdrPath{};
    bool headless = false;
};

class SELAH_API Application : public Window::ICallbacks
{
public:
public:
    Application(const AppConfig&);
    void run();
    virtual void onInitialize() {}
    virtual void onLoad() {}           // assets resources
    virtual void onUpdate(float dt) {} // animation
    virtual void onFixedUpdate() {}    // physics, AI, etc
    virtual void onShutdown() {}
    virtual void onResize() {}
    virtual void onFrameRender() {}
    virtual void onGuiRender() {}

    virtual ~Application() = default;

    virtual bool onKeyboardEvent(const KeyboardEvent& keyEvent) { return false; }
    virtual bool onMouseEvent(const MouseEvent& mouseEvent) { return false; }
    virtual void onGamepadEvent(const GamepadEvent& gamepadEvent) {}
    virtual void onGamepadState(const GamepadState& gamepadState) {}
    virtual void onDroppedFile(const std::filesystem::path& path) {}

    AppConfig config;

private:
    void initialize();
    void initializeUI();
    void initializeBackend();
    void update(float dt);
    void fixedUpdate();
    void renderFrame();
    void shutDown();
    void shutDownUI();
    void shutDownBackend();
    void updateUI();
    void updateAndRenderUIMultipleWindows();
    bool initialized{false};

    std::unique_ptr<Window> m_window;

    void handleWindowSizeChange() override;
    void handleMouseEvent(const MouseEvent& mouseEvent) override;
    void handleKeyboardEvent(const KeyboardEvent& keyEvent) override;
    void handleGamepadEvent(const GamepadEvent& gamepadEvent) override;
    void handleGamepadState(const GamepadState& gamepadState) override;
    void handleDroppedFile(const std::filesystem::path& path) override;

protected:
    ExampleDescriptorHeapAllocator srvDescHeapAlloc;

    struct FrameContext
    {
        ID3D12CommandAllocator* CommandAllocator;
        UINT64 FenceValue;
    };

    // Config for example app
    // frames in flight == back buffers, for simplicity
    static const int APP_NUM_FRAMES_IN_FLIGHT = 2;
    static const int APP_NUM_BACK_BUFFERS = 2;
    static const int APP_SRV_HEAP_SIZE = 64;

    FrameContext frameContext[APP_NUM_FRAMES_IN_FLIGHT] = {};
    UINT frameIndex = 0;
    void* constantData;

    ID3D12Device* device = nullptr;
    ID3D12DescriptorHeap* rtvDescHeap = nullptr;
    ID3D12DescriptorHeap* srvDescHeap = nullptr;
    ID3D12DescriptorHeap* dsvDescHeap = nullptr;
    ID3D12CommandQueue* directQueue = nullptr;
    ID3D12CommandQueue* copyQueue = nullptr;
    ID3D12GraphicsCommandList* mainCommandList = nullptr;
    ID3D12Fence* fence = nullptr;
    HANDLE fenceEvent = nullptr;
    IDXGISwapChain3* pSwapChain = nullptr;
    HANDLE hSwapChainWaitableObject = nullptr;

    ID3D12Resource* mainRenderTargetResource[APP_NUM_BACK_BUFFERS] = {};
    ID3D12Resource* mainLinearTargetResource[APP_NUM_BACK_BUFFERS] = {};
    ID3D12Resource* depthStencil = nullptr;

    D3D12_CPU_DESCRIPTOR_HANDLE mainRenderTargetDescriptor[APP_NUM_BACK_BUFFERS] = {};
    D3D12_CPU_DESCRIPTOR_HANDLE mainLinearRenderTargetDescriptor[APP_NUM_BACK_BUFFERS] = {};
    D3D12_CPU_DESCRIPTOR_HANDLE mainLinearHandleCPU[APP_NUM_BACK_BUFFERS] = {};
    D3D12_GPU_DESCRIPTOR_HANDLE mainLinearHandleGPU[APP_NUM_BACK_BUFFERS] = {};

    void cleanupRenderTarget();
    void cleanupDeviceD3D();
    bool createDeviceD3D(HWND hwnd);
    void createRenderTarget();

    void waitForGpu();
    void moveToNextFrame();
    UINT fenceValues[APP_NUM_FRAMES_IN_FLIGHT];

    void renderUI();
    bool bShowUI = true;
};

} // namespace SL
